﻿Imports Microsoft.Xna.Framework
Imports System.Collections.Generic

''' <summary>
''' Calculates View and Projection matrices.
''' Depending on the camera mode, it should move itself - following a character
''' or allow the user to move it.
''' </summary>
''' <remarks>
''' TODO:
''' 
''' 1.) In 1st and 3rd person modes, allow limited movement based on user input.
''' 
''' 2.) Constrain the camera position at all times to what is possible - i.e. not
'''     going through the terrain or game objects.
''' 
''' 3.) Extract the physics info from the MovableObject class so that MovableObject
'''     will have PhysicsInfo. Next, make Camera3D have PhysicsInfo.
''' 
''' 4.) Implement ThirdPersonSpring camera mode. The camera should act as though 
'''     tied to the location of the TrackedCharacter via a spring (would be using 
'''     PhysicsInfo).
''' 
''' 5.) Implement CameraPerson camera mode. In this mode, the camra should try and
'''     move itself to frame a sensible shot. It should behave a bit more 
'''     intelligently, moving around objects to prevent obstructed view.
''' </remarks>
Public Class Camera3D
    Inherits XNAObject

#Region " Fields "

    Private mTrackedCharacter As ITrackableObject
    Private mCameraMode As CameraMode = CameraMode.FreeRoam
    Private mIsActive As Boolean = True
    Private mAllowMovement As Boolean = True

    Private mMovement As Vector4
    Const mForwardSpeed As Single = 50.0F
    Const mRotationSpeed = 90.0F

    Private mVelocity As Vector3

    Private mFieldOfView = MathHelper.ToRadians(45)
    Private mNearPlaneDistance As Single = 1.0F
    Private mFarPlaneDistance As Single = 10000.0F
    Private mAspectRatio As Single = 1.33F

    Private mDesiredPosition As Vector3
    Private mDesiredPositionOffset As New Vector3(0, 2.0F, 2.0F)
    Private mLookAtOffset As New Vector3(0, 2.8F, 0)
    Private mLookAt As Vector3

    Private mStiffness As Single = 1800.0F
    Private mDamping As Single = 600.0F
    Private mMass As Single = 50.0F

    Private mView As Matrix
    Private mProjection As Matrix
    Private mBoundingFrustum As BoundingFrustum

    Private mCcp As ICollisionCandidateProvider

#End Region ' Fields

    Public Sub New(ByVal GameManager As GameManager)
        MyBase.New(GameManager)
    End Sub

    Public Sub New(ByVal GameManager As GameManager, ByVal ccp As ICollisionCandidateProvider)
        MyBase.New(GameManager)
        mCcp = ccp
    End Sub

#Region " Camera Properties "

    Public Property TrackedCharacter() As ITrackableObject
        Get
            TrackedCharacter = mTrackedCharacter
        End Get
        Set(ByVal value As ITrackableObject)
            ' Free roam is the only valid camera mode if 
            ' we do not have a character to track
            If value Is Nothing Then
                mCameraMode = CameraMode.FreeRoam
            End If

            mTrackedCharacter = value
        End Set
    End Property

    Public Property CameraMode() As CameraMode
        Get
            CameraMode = mCameraMode
        End Get
        Set(ByVal value As CameraMode)
            ' If the supplied value is no different from 
            ' the current mode then do nothing
            If value = mCameraMode Then Exit Property

            ' Free roam is the only valid camera mode if 
            ' we do not have a character to track
            If TrackedCharacter Is Nothing Then
                mCameraMode = GameEngine.CameraMode.FreeRoam
            Else
                mCameraMode = value
                Reset()
            End If
        End Set
    End Property

    Public Property IsActive() As Boolean
        Get
            IsActive = mIsActive
        End Get
        Set(ByVal value As Boolean)
            mIsActive = value
        End Set
    End Property

    Public Property AllowMovement() As Boolean
        Get
            AllowMovement = mAllowMovement
        End Get
        Set(ByVal value As Boolean)
            mAllowMovement = value
        End Set
    End Property

#End Region ' Properties

#Region "Desired camera positioning (set when creating camera or changing view)"

    ''' <summary>
    ''' Desired camera position in the chased object's coordinate system.
    ''' </summary>
    Public Property DesiredPositionOffset() As Vector3
        Get
            Return mDesiredPositionOffset
        End Get
        Set(ByVal value As Vector3)
            mDesiredPositionOffset = value
        End Set
    End Property

    ''' <summary>
    ''' Desired camera position in world space.
    ''' </summary>
    Public ReadOnly Property DesiredPosition() As Vector3
        Get
            Return mDesiredPosition
        End Get
    End Property

    ''' <summary>
    ''' Look at point in the tracked object's coordinate system.
    ''' </summary>
    Public Property LookAtOffset() As Vector3
        Get
            Return mLookAtOffset
        End Get
        Set(ByVal value As Vector3)
            mLookAtOffset = value
        End Set
    End Property

    ''' <summary>
    ''' Look at point in world space.
    ''' </summary>
    Public ReadOnly Property LookAt() As Vector3
        Get
            Return mLookAt
        End Get
    End Property

#End Region

#Region "Camera physics (typically set when creating camera)"

    ''' <summary>
    ''' Physics coefficient which controls the influence of the camera's position
    ''' over the spring force. The stiffer the spring, the closer it will stay to
    ''' the chased object.
    ''' </summary>
    Public Property Stiffness() As Single
        Get
            Return mStiffness
        End Get
        Set(ByVal value As Single)
            mStiffness = value
        End Set
    End Property

    ''' <summary>
    ''' Physics coefficient which approximates internal friction of the spring.
    ''' Sufficient damping will prevent the spring from oscillating infinitely.
    ''' </summary>
    Public Property Damping() As Single
        Get
            Return mDamping
        End Get
        Set(ByVal value As Single)
            mDamping = value
        End Set
    End Property

    ''' <summary>
    ''' Mass of the camera body. Heaver objects require stiffer springs with less
    ''' damping to move at the same rate as lighter objects.
    ''' </summary>
    Public Property Mass() As Single
        Get
            Return mMass
        End Get
        Set(ByVal value As Single)
            mMass = value
        End Set
    End Property

#End Region

#Region "Current camera properties (updated by camera physics)"

    ''' <summary>
    ''' Velocity of camera.
    ''' </summary>
    Public ReadOnly Property Velocity() As Vector3
        Get
            Return mVelocity
        End Get
    End Property

    ''' <summary>
    ''' View transform matrix.
    ''' </summary>
    Public ReadOnly Property View() As Matrix
        Get
            If Not MatricesValid Then UpdateMatrices()
            View = mView
        End Get
    End Property

    ''' <summary>
    ''' Projecton transform matrix.
    ''' </summary>
    Public ReadOnly Property Projection() As Matrix
        Get
            If Not MatricesValid Then UpdateMatrices()
            Projection = mProjection
        End Get
    End Property

    ''' <summary> 
    ''' BoundingFrustum, used for Culling 
    ''' </summary> 
    Public ReadOnly Property BoundingFrustum() As BoundingFrustum
        Get
            If Not MatricesValid Then UpdateMatrices()
            BoundingFrustum = mBoundingFrustum
        End Get
    End Property
#End Region

#Region "Perspective properties"

    ''' <summary>
    ''' Perspective aspect ratio. Default value should be overriden by application.
    ''' </summary>
    Public Property AspectRatio() As Single
        Get
            Return mAspectRatio
        End Get
        Set(ByVal value As Single)
            mAspectRatio = value
        End Set
    End Property

    ''' <summary>
    ''' Perspective field of view.
    ''' </summary>
    Public Property FieldOfView() As Single
        Get
            Return mFieldOfView
        End Get
        Set(ByVal value As Single)
            mFieldOfView = value
        End Set
    End Property

    ''' <summary>
    ''' Distance to the near clipping plane.
    ''' </summary>
    Public Property NearPlaneDistance() As Single
        Get
            Return mNearPlaneDistance
        End Get
        Set(ByVal value As Single)
            mNearPlaneDistance = value
        End Set
    End Property

    ''' <summary>
    ''' Distance to the far clipping plane.
    ''' </summary>
    Public Property FarPlaneDistance() As Single
        Get
            Return mFarPlaneDistance
        End Get
        Set(ByVal value As Single)
            mFarPlaneDistance = value
        End Set
    End Property

#End Region

#Region "Public Methods"

    ''' <summary>
    ''' Forces camera to be at desired position and to stop moving. The is useful
    ''' when the chased object is first created or after it has been teleported.
    ''' Failing to call this after a large change to the chased object's position
    ''' will result in the camera quickly flying across the world.
    ''' </summary>
    Public Sub Reset()
        ' Figure out where the camera should be and move there
        Select Case CameraMode
            Case GameEngine.CameraMode.FirstPerson
                MoveTo(TrackedCharacter.Position, 1.0F)
                Rotation = mTrackedCharacter.Rotation
            Case GameEngine.CameraMode.ThirdPerson
                MoveTo(TrackedCharacter.Position, 1.0F)
                Rotation = mTrackedCharacter.Rotation
                Move(DesiredPositionOffset)
            Case GameEngine.CameraMode.FreeRoam
                ConstrainYAxis(1.0F)
        End Select

        ' Stop motion
        mVelocity = Vector3.Zero

    End Sub


    ''' <summary>
    ''' Calls the appropriate Update method - depending on the current Camera Mode
    ''' </summary>
    Public Sub Update(ByVal elapsedSeconds As Single)
        Select Case CameraMode
            Case GameEngine.CameraMode.FreeRoam
                UpdateCameraFreeRoam(elapsedSeconds)
            Case GameEngine.CameraMode.FirstPerson
                UpdateCameraFirstPerson(elapsedSeconds)
            Case GameEngine.CameraMode.ThirdPerson
                UpdateCameraThirdPerson(elapsedSeconds)
                ' TODO
                'Case GameEngine.CameraMode.ThirdPersonSpring
                'UpdateCameraThirdPersonSpring(elapsedSeconds)
                'Case GameEngine.CameraMode.CameraPerson
                'UpdateCameraCameraPerson(elapsedSeconds) 
        End Select

        If mCcp IsNot Nothing AndAlso mCcp.HeightMapInfo.IsOnHeightmap(Position) Then
            ' we don't want the camera to go beneath the terrain's height +
            ' a small offset.
            Dim minimumHeight As Single = mCcp.HeightMapInfo.GetHeight(Position) '+ CameraPositionOffset.Y

            If Position.Y < minimumHeight Then
                PositionY = minimumHeight
            End If
        End If

        UpdateMatrices()
    End Sub

    Public Overridable Sub HandleInput(ByVal input As IGamePadInputManager)
        If mAllowMovement Then
            mMovement = New Vector4(input.SecondaryMovement, input.LeftTrigger - input.RightTrigger, 0)
        End If

        ' Cycle through the camera modes
        If input.RightShoulderPressed Then
            If CameraMode = 2 Then
                CameraMode = 0
            Else
                CameraMode += 1
            End If
            GameManager.ShowNotification("Camera Mode: " & CameraMode.ToString)
            Reset()
        End If
    End Sub

#End Region ' Public Methods

#Region "Private Methods"

    ''' <summary>
    ''' Rebuilds camera's view and projection matricies and the bounding frustrum (if Active Camera).
    ''' </summary>
    Private Sub UpdateMatrices()
        'Re-Creates Matrices 
        If Not IsActive Then Return

        mView = Matrix.Invert(Matrix.CreateFromQuaternion(Rotation) * Matrix.CreateTranslation(Position))
        mProjection = Matrix.CreatePerspectiveFieldOfView(FieldOfView, AspectRatio, NearPlaneDistance, FarPlaneDistance)

        mBoundingFrustum = New BoundingFrustum(mView * mProjection)

        MatricesValid = True
    End Sub

    Private Sub UpdateCameraFreeRoam(ByVal elapsedSeconds As Single)

        RotateRightAroundUp(mMovement.X * elapsedSeconds * mRotationSpeed)
        PitchDown(mMovement.Y * elapsedSeconds * mRotationSpeed)
        MoveBackwards(mMovement.Z * elapsedSeconds * mForwardSpeed)


        Dim rotationMatrix As Matrix = Matrix.CreateFromQuaternion(Rotation)

        ' Create a vector pointing the direction the camera is facing.
        Dim transformedReference As Vector3 = Vector3.Transform(Vector3.Forward, rotationMatrix)

        ' Calculate the position the camera is looking at.
        mLookAt = Position + transformedReference

    End Sub

    ''' <summary> 
    ''' Follow Target Object
    ''' </summary>
    Private Sub UpdateCameraFirstPerson(ByVal elapsedSeconds As Single)
        Rotation = mTrackedCharacter.Rotation
        Position = mTrackedCharacter.Position

        Dim rotationMatrix As Matrix = Matrix.CreateFromQuaternion(Rotation)

        ' Create a vector pointing the direction the camera is facing.
        Dim transformedReference As Vector3 = Vector3.Transform(Vector3.Forward, rotationMatrix)

        '' Calculate the position the camera is looking at.
        mLookAt = Position * transformedReference

    End Sub

    ''' <summary> 
    ''' Follow Target Object at the specified distance
    ''' </summary>
    Private Sub UpdateCameraThirdPerson(ByVal elapsedSeconds As Single)
        Position = mTrackedCharacter.Position
        Rotation = mTrackedCharacter.Rotation

        ' Adjust for the camera offset
        Move(DesiredPositionOffset)

        'Turn to towards the chased object
        LookAtPoint(mTrackedCharacter.Position, 1)
    End Sub

    '''' <summary>
    '''' Animates the camera from its current position towards the desired offset
    '''' behind the chased object. The camera's animation is controlled by a simple
    '''' physical spring attached to the camera and anchored to the desired position.
    '''' </summary>
    'Private Sub UpdateCameraThirdPersonSpring(ByVal elapsedSeconds As Single)

    '    Rotation = TrackedCharacter.Rotation

    '    ' Calculate spring force
    '    Dim stretch As Vector3 = Position - mDesiredPosition
    '    Dim force As Vector3 = -mStiffness * stretch - mDamping * mVelocity

    '    ' Apply acceleration
    '    Dim acceleration As Vector3 = force / mMass
    '    mVelocity = acceleration * elapsedSeconds

    '    ' Apply velocity
    '    Move(mVelocity * elapsedSeconds)
    '    'Position += mVelocity * elapsedSeconds

    '    Dim rotationMatrix As Matrix = Matrix.CreateFromQuaternion(Rotation)

    '    ' Create a vector pointing the direction the camera is facing.
    '    Dim transformedReference As Vector3 = Vector3.Transform(Vector3.Forward, rotationMatrix)

    '    ' Construct a matrix to transform from object space to worldspace
    '    Dim transform As Matrix = Matrix.CreateFromQuaternion(Rotation)

    '    ' Calculate desired camera properties in world space
    '    mDesiredPosition = TrackedCharacter.Position + Vector3.TransformNormal(DesiredPositionOffset, transform)
    '    mLookAt = TrackedCharacter.Position + Vector3.TransformNormal(LookAtOffset, transform)

    '    'Position = mTrackedCharacter.Position
    '    'Rotation = mTrackedCharacter.Rotation

    '    '' Adjust for the camera offset
    '    'Move(DesiredPositionOffset)

    '    ''Turn to towards the chased object
    '    'LookAtPoint(mTrackedCharacter.Position, 1)
    'End Sub

    ''Private Sub UpdateCameraCameraPerson(ByVal elapsedSeconds As Single)
    ''End Sub

#End Region ' Private Methods

End Class


Public Enum CameraMode
    FirstPerson
    ThirdPerson
    'ThirdPersonSpring
    'CameraPerson
    FreeRoam
End Enum